package com.google.code.stalkernet.chat;

import java.io.File;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import com.google.code.stalkernet.advertisements.Advertisements;
import com.google.code.stalkernet.advertisements.SimpleAdvertisements;
import com.google.code.stalkernet.async.factory.AsyncSocketAddressPeerFactory;
import com.google.code.stalkernet.async.factory.AsyncSocketChannelPeerFactory;
import com.google.code.stalkernet.chat.ui.MainFrame;
import com.google.code.stalkernet.database.Database;
import com.google.code.stalkernet.database.InMemoryDatabase;
import com.google.code.stalkernet.example.ui.util.SocketAddressUtils;
import com.google.code.stalkernet.node.Node;
import com.google.code.stalkernet.node.SimpleNode;
import com.google.code.stalkernet.peer.Peer;
import com.google.code.stalkernet.reactor.Reactor;
import com.google.code.stalkernet.reactor.SelectorReactor;
import com.google.code.stalkernet.server.ReactorServer;
import com.google.code.stalkernet.service.AdvertisementConnectorService;
import com.google.code.stalkernet.service.DatabasePersistanceService;

/**
 * Main class of Stalker.Net Chat application.
 *
 * @author Mikhail Vladimirov
 */
public class StalkerNetChat implements Runnable
{
    private final SelectorReactor reactor;
    private final Database database;
    private final Advertisements advertisements;
    private final Node node;
    private final AsyncSocketChannelPeerFactory asyncSocketChannelPeerFactory;
    private final ReactorServer reactorServer;
    private final DatabasePersistanceService databasePersistanceService;
    private final AsyncSocketAddressPeerFactory <? extends Peer>
        asyncSocketAddressPeerFactory;
    private final AdvertisementConnectorService advertisementConnectorService;

    /**
     * Create new Stalker.Net Chat with given database file.
     *
     * @param databaseFile {@link File} to store database in
     * @throws IOException if I/O error occurred
     */
    public StalkerNetChat (
        SocketAddress [] listenAddresses,
        SocketAddress [] connectAddresses,
        File databaseFile) throws IOException
    {
        if (listenAddresses == null)
            throw new IllegalArgumentException ("Listen addresses is null");

        if (databaseFile == null)
            throw new IllegalArgumentException ("Database file is null");

        reactor = new SelectorReactor (Selector.open ());
        database = new InMemoryDatabase ();
        advertisements = new SimpleAdvertisements ();
        node = new SimpleNode (
            database, advertisements);
        asyncSocketChannelPeerFactory = new AsyncSocketChannelPeerFactory (
            node.getID (), reactor);

        reactorServer = new ReactorServer (
            reactor, asyncSocketChannelPeerFactory, node);
        for (SocketAddress listenAddress: listenAddresses)
            reactorServer.addSocketAddress (listenAddress);

        databasePersistanceService = new DatabasePersistanceService (
            databaseFile, database);
        asyncSocketAddressPeerFactory = new AsyncSocketAddressPeerFactory <> (
            asyncSocketChannelPeerFactory, reactor);
        advertisementConnectorService = new AdvertisementConnectorService (
            reactor, advertisements,
            asyncSocketAddressPeerFactory, node);

        for (SocketAddress connectAddress: connectAddresses)
            advertisementConnectorService.connect (connectAddress);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run ()
    {
        databasePersistanceService.start ();
        advertisementConnectorService.start ();

        MainFrame mainFrame = new MainFrame (
            database, new ReactorExecutor (reactor),
            System.getProperty ("user.name"));
        mainFrame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        mainFrame.pack ();
        mainFrame.setVisible (true);

        new Thread (reactor, "Reactor").start ();
    }

    /**
     * Entry point of Stalker.Net chat.
     *
     * @param args command-line arguments
     * @throws Exception if an error occurred
     */
    public static void main (String [] args) throws Exception
    {
        if (args == null)
            throw new IllegalArgumentException ("Args is null");

        List <SocketAddress> listenAdresses = new ArrayList <> ();
        List <SocketAddress> connectAdresses = new ArrayList <> ();

        List <String> nonFlagArgs = new ArrayList <> ();

        int state = 0;
        for (String arg: args)
        {
            switch (state)
            {
            case 0: // Initial
                if ("-c".equals (arg) || "--connect".equals (arg))
                {
                    state = 3;
                    break;
                }
                if ("-l".equals (arg) || "--listen".equals (arg))
                {
                    state = 1;
                    break;
                }
                else if ("--".equals (arg))
                {
                    state = 2;
                    break;
                }
                else if (arg.startsWith ("-"))
                {
                    throw new IllegalArgumentException (
                        "Unknown option: " + arg);
                }
                else
                {
                    nonFlagArgs.add (arg);
                    state = 2;
                    break;
                }
            case 1: // Listen
                listenAdresses.add (
                    SocketAddressUtils.parseSocketAddress (arg));
                state = 0;
                break;
            case 2: // Non-flag arguments
                nonFlagArgs.add (arg);
                break;
            case 3: // Connect
                connectAdresses.add (
                    SocketAddressUtils.parseSocketAddress (arg));
                state = 0;
                break;
            default:
                throw new Error ("Impossible");
            }
        }

        File dir;
        switch (nonFlagArgs.size ())
        {
        case 0:
            dir = new File (System.getProperty ("user.home"), ".stalkerchat");
            break;
        case 1:
            dir = new File (nonFlagArgs.get (0));
            break;
        default:
            throw new IllegalArgumentException (
                "Too many command-line arguments");
        }

        dir.mkdirs ();

        StalkerNetChat stalkerNetChat = new StalkerNetChat (
            listenAdresses.toArray (new SocketAddress [listenAdresses.size ()]),
            connectAdresses.toArray (
                new SocketAddress [connectAdresses.size ()]),
            new File (dir, "chat.db"));
        SwingUtilities.invokeLater (stalkerNetChat);
    }

    private static class ReactorExecutor implements Executor
    {
        private final Reactor reactor;

        public ReactorExecutor (Reactor reactor)
        {
            if (reactor == null)
                throw new IllegalArgumentException ("Reactor is null");

            this.reactor = reactor;
        }

        @Override
        public void execute (Runnable command)
        {
            if (command == null)
                throw new IllegalArgumentException ("Command is null");

            reactor.invoke (command);
        }
    }
}
